package com.panopset.io;

import java.io.File;
import java.io.StringWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import java.util.TreeMap;

import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.panopset.compat.Strings;

/**
 * Parse multiple XML files and produce a report with statistics.
 *
 * @author Karl Dinwiddie
 *
 */
public final class XMLDataStats {

    /**
     * @param reportFile
     *            , usually a csv.
     * @param separator
     *            Separator.
     */
    public void generateReport(final File reportFile, final String separator) {
        sep = separator;
        LockedWriter lw = new LockedWriter(reportFile);
        for (Entry<String, Elem> e : elems.entrySet()) {
            if (e.getValue().hasValues()) {
                lw.writeln(e.getValue().toString());
            }
        }
        lw.close();
    }

    /**
     * @param f
     *            XML file to process.
     * @throws Exception
     *             exception.
     */
    public void process(final File f) throws Exception {
        SAXParserFactory.newInstance().newSAXParser()
                .parse(f, new DefaultHandler() {
                    /**
                     * Parent chain.
                     */
                    private final Stack<Elem> chain = new Stack<Elem>();

                    @Override
                    public void startElement(final String uri,
                            final String localName, final String qName,
                            final Attributes attributes) throws SAXException {
                        if (isSuppressed(qName)) {
                            return;
                        }
                        String key = createKey(qName, attributes, chain);
                        Elem elem = elems.get(key);
                        if (elem == null) {
                            elem = new Elem(key, qName, attributes);
                            elems.put(key, elem);
                        }
                        chain.push(elem);
                    }

                    @Override
                    public void characters(final char[] ch, final int start,
                            final int length) throws SAXException {
                        if (chain == null) {
                            return;
                        }
                        Elem elem = chain.peek();
                        if (elem == null) {
                            return;
                        }
                        if (elem.hasAttrs()) {
                            elem.addValue(new String(ch, start, length));
                        }
                    }

                    @Override
                    public void endElement(final String uri,
                            final String localName, final String qName)
                            throws SAXException {
                        if (isSuppressed(qName)) {
                            return;
                        }
                        chain.pop();
                    }
                });

    }

    /**
     * Element data data structure.
     */
    private final Map<String, Elem> elems = Collections
            .synchronizedMap(new TreeMap<String, Elem>());

    /**
     *
     * Unique XML element.
     *
     */
    private final class Elem {

        @Override
        public String toString() {
            StringWriter sw = new StringWriter();
            sw.append(">key*>");
            sw.append(getKey());
            sw.append(sep);
            sw.append(">val*>");
            long count = 0;
            FOR: for (Entry<String, Integer> e : values.entrySet()) {
                sw.append(e.getKey());
                sw.append("**");
                sw.append("" + e.getValue());
                sw.append("**");
                if (count++ > max) {
                    break FOR;
                }
            }
            return sw.toString();
        }

        /**
         * qname.
         */
        private final String qnm;
        /**
         * Attributes.
         */
        private final Attributes attrs;

        /**
         * Map values to count of unique occurrences.
         */
        private final Map<String, Integer> values

        = new HashMap<String, Integer>();

        /**
         * @return True iff this element has values.
         */
        private boolean hasValues() {
            return values != null && values.size() > 0;
        }

        /**
         * Constructor.
         *
         * @param key
         *            Key.
         * @param qName
         *            qname.
         * @param attributes
         *            attributes.
         */
        Elem(final String key, final String qName,

        final Attributes attributes) {
            k = key;
            qnm = qName;
            attrs = attributes;
        }

        /**
         * @return true iff the element has attributes.
         */
        private boolean hasAttrs() {
            if (attrs == null) {
                return false;
            }
            return attrs.getLength() > 0;
        }

        /**
         * Key.
         */
        private final String k;

        /**
         *
         * @return Unique key for this element.
         */
        private String getKey() {
            return k;
        }

        /**
         * Keep track of the number of times an element value is encountered, in
         * a single or multiple XML files.
         *
         * @param s
         *            Value.
         */
        private void addValue(final String s) {
            Integer count = values.get(s);
            if (count == null) {
                values.put(s, 1);
            } else {
                values.put(s, ++count);
            }
        }
    }

    /**
     * @param qnm
     *            Qname.
     * @param attrs
     *            Attributes.
     * @return Long path, or empty String if doesn't match paths set by setPaths
     *         method.
     */
    private String getLongPath(final String qnm, final Attributes attrs) {
        StringWriter sw = new StringWriter();
        sw.append(qnm);
        sw.append(sep);
        for (int i = 0; i < attrs.getLength(); i++) {
            sw.append(attrs.getQName(i));
            sw.append(sep);
            sw.append(attrs.getValue(i));
            sw.append(sep);
        }
        return sw.toString();
    }

    /**
     * @param qnm
     *            Qname.
     * @param attrs
     *            Attributes.
     * @return Short path, or empty String if doesn't match paths set by
     *         setPaths method.
     */
    private String getShortPath(final String qnm, final Attributes attrs) {
        StringWriter sw = new StringWriter();
        if (paths != null) {
            for (String[] t : paths) {
                if (t[0].equals(qnm)) {
                    for (int i = 0; i < attrs.getLength(); i++) {
                        if (attrs.getQName(i).equals(t[1])) {
                            sw.append(attrs.getValue(t[1]));
                            sw.append(sep);
                        }
                    }
                }
            }
        }
        return sw.toString();
    }

    /**
     * Create an element key.
     *
     * @param qnm
     *            Qname.
     * @param attrs
     *            Attributes.
     * @param chain
     *            Parent chain.
     * @return Key.
     */
    private String createKey(final String qnm, final Attributes attrs,
            final Stack<Elem> chain) {
        StringWriter sw = new StringWriter();
        StringWriter sw0 = new StringWriter();
        for (Elem e : chain) {
            sw0.append(getShortPath(e.qnm, e.attrs));
            sw.append(getLongPath(e.qnm, e.attrs));
        }
        sw0.append(getShortPath(qnm, attrs));
        sw.append(getLongPath(qnm, attrs));
        if (Strings.isPopulated(sw0.toString())) {
            return sw0.toString();
        } else {
            sw.append(qnm);
            return sw.toString();
        }
    }

    /**
     * Generate a comma separated report.
     *
     * @param reportFile
     *            , usually a csv.
     */
    public void generateReport(final File reportFile) {
        generateReport(reportFile, ",");
    }

    /**
     * 5.
     */
    private static final long DEFAULT_MAX = 5;

    /**
     * Maximum samples.
     */
    private long max = DEFAULT_MAX;

    /**
     * Separator, default is comma.
     */
    private String sep = ",";

    /**
     * @param value
     *            Maximum samples, default is 5.
     */
    public void setMaxSamples(final long value) {
        max = value;
    }

    /**
     *
     * @param v
     *            Value (qname).
     * @return true iff suppressed qname.
     */
    private boolean isSuppressed(final String v) {
        if (suppr == null || v == null) {
            return false;
        }
        for (String s : suppr) {
            if (v.equals(s)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Suppressions.
     */
    private String[] suppr;

    /**
     * @param suppressions
     *            Suppress these elements.
     */
    public void setSuppress(final String[] suppressions) {
        suppr = suppressions;
    }

    /**
     * Path values.
     */
    private String[][] paths;

    /**
     * @param pathValues
     *            Array of element, key attribute pairs.
     */
    public void setPaths(final String[][] pathValues) {
        paths = pathValues;
    }
}
